package com.neuronbit.businessflow.flow.source;

import com.neuronbit.businessflow.flow.parser.FlowNode;
import com.neuronbit.businessflow.flow.parser.Output;
import com.neuronbit.businessflow.flow.source.util.FieldBuilder;
import com.neuronbit.businessflow.flow.source.util.Imports;
import com.neuronbit.businessflow.intellij.model.Param;
import com.neuronbit.businessflow.intellij.model.Return;
import com.neuronbit.businessflow.intellij.model.ExceptionHandler;
import com.neuronbit.businessflow.runtime.Flow;
import com.neuronbit.businessflow.runtime.StepBeanProvider;
import com.neuronbit.javaparser.JavaParser;
import com.neuronbit.javaparser.ast.CompilationUnit;
import com.neuronbit.javaparser.ast.Modifier;
import com.neuronbit.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.neuronbit.javaparser.ast.body.ConstructorDeclaration;
import com.neuronbit.javaparser.ast.body.MethodDeclaration;
import com.neuronbit.javaparser.ast.expr.LambdaExpr;
import com.neuronbit.javaparser.ast.stmt.BlockStmt;
import com.neuronbit.javaparser.ast.stmt.ReturnStmt;
import com.neuronbit.javaparser.ast.type.ClassOrInterfaceType;
import lombok.Setter;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;

import java.util.List;
import java.util.Optional;
import java.util.concurrent.Future;

import static java.lang.String.format;
import static org.apache.commons.lang3.StringUtils.capitalize;

/**
 * The type Flow node source generator.
 */
public class FlowJavaSource implements JavaSource {
    private final FlowParamJavaSource flowParamSource;
    private final FlowNode flow;
    private final ContextJavaSource contextSource;
    private ClassOrInterfaceDeclaration flowImplClass;
    @Setter
    private FLowEntryStep entryStep;

    /**
     * Instantiates a new Flow node source generator.
     *
     * @param flow            the flow
     * @param contextSource   the context source
     * @param flowParamSource the flow param source
     * @throws ClassNotFoundException the class not found exception
     * @throws NoSuchMethodException  the no such method exception
     */
    public FlowJavaSource(FlowNode flow, ContextJavaSource contextSource, FlowParamJavaSource flowParamSource) throws ClassNotFoundException, NoSuchMethodException {
        this.flowParamSource = flowParamSource;
        this.flow = flow;
        this.contextSource = contextSource;
    }

    /**
     * 1. 生成 context
     * 2. 生成 ParameterMapper，除了 Mapper，校验的逻辑应该在这里触发
     * 3. 生成整个 flow 调用的代码
     * 4. stop on,如果每一步返回的 output 中，都包含一个叫做 error code 的字段，在 stop on 中，对整个 error code进行检测，如果 error code 不等于0就终止执行，应该怎么处理呢？
     * 这个 error code 也是整个 flow 的 output 中的字段。
     */
    public void generate() throws ClassNotFoundException, NoSuchMethodException {
        String flowParamClassName = flowParamSource.getSimpleName();

        CompilationUnit flowCU = flow.getCompilationUnit();
        Optional<ClassOrInterfaceDeclaration> flowInterface = flowCU.getInterfaceByName(flow.getType().getName());
        flowInterface.ifPresent(flowCU::remove);

        if (!flowInterface.isPresent()) {
            throw new ClassNotFoundException(flow.getType().getName());
        }

        List<MethodDeclaration> methodsByName = flowInterface.get().getMethodsByName(flow.getMethodName());
        if (methodsByName.isEmpty()) {
            throw new NoSuchMethodException(flow.getType().getName() + "." + flow.getMethodName());
        }

        MethodDeclaration entryMethod = null;
        for (MethodDeclaration methodDeclaration : methodsByName) {
            if (methodDeclaration.getParameters().size() == flow.getParamCount()) {
                entryMethod = methodDeclaration;
            }
        }

        if (null == entryMethod) {
            throw new NoSuchMethodException(flow.getType().getName() + "." + flow.getMethodName() + " has " + flow.getParamCount() + " parameters");
        }

        entryMethod.getThrownExceptions();

        flowCU.addImport(Flow.class);
        flowImplClass = flowCU.addClass(flow.getName() + "Impl");
        flowImplClass.addImplementedType(flow.getType().getName());

        List<MethodDeclaration> startOpt = flowImplClass.getMethodsByName(entryMethod.getNameAsString());
        if (CollectionUtils.isNotEmpty(startOpt)) {
            for (MethodDeclaration md : startOpt) {
                flowImplClass.remove(md);
            }
        }

        String name = flow.getName();
        String description = flow.getDescription();
        ConstructorDeclaration constructor = flowImplClass.addConstructor(Modifier.Keyword.PUBLIC);
        flowCU.addImport(StepBeanProvider.class);
        constructor.addParameter(StepBeanProvider.class, "stepBeanProvider");
        constructor.addParameter(String.class, "id");
        constructor.setBody(new BlockStmt());
        constructor.getBody().addStatement(format("super(\"%s\", \"%s\", stepBeanProvider, id);", name, description));

        MethodDeclaration reRunMethod = flowImplClass.addMethod("reRun", Modifier.Keyword.PUBLIC, Modifier.Keyword.FINAL);
        reRunMethod.addParameter(contextSource.getSimpleName(), "context");
        reRunMethod.addParameter(boolean.class, "force");

        MethodDeclaration startMethod = flowImplClass.addMethod(entryMethod.getNameAsString(), Modifier.Keyword.PUBLIC, Modifier.Keyword.FINAL);
        MethodDeclaration startAsyncMethod = flowImplClass.addMethod(entryMethod.getNameAsString() + "Async", Modifier.Keyword.PUBLIC, Modifier.Keyword.FINAL);
        if (MapUtils.isNotEmpty(flow.getCallable().getParams())) {
            for (Param input : flow.getCallable().getParams().values()) {
                Imports.of(input.getType()).install(flowCU);
                startMethod.addParameter(input.getType().getName(), input.getName());
                startAsyncMethod.addParameter(input.getType().getName(), input.getName());
            }
        }

        flowCU.addImport(Future.class);
        if (null != flow.getOutput()) {
            Imports.of(flow.getOutput().getType()).install(flowCU);
            startMethod.setType(flow.getOutput().getType().describe());
            reRunMethod.setType(flow.getOutput().getTypeName());

            ClassOrInterfaceType returnType = new ClassOrInterfaceType(null, "Future")
                            .setTypeArguments(new ClassOrInterfaceType(null, flow.getOutput().getTypeName()));
            startAsyncMethod.setType(returnType);

            ClassOrInterfaceType superType = new ClassOrInterfaceType(null, "Flow")
                    .setTypeArguments(new ClassOrInterfaceType(null, contextSource.getSimpleName()),
                            new ClassOrInterfaceType(null, flow.getOutput().getTypeName()));
            flowImplClass.addExtendedType(superType);
        } else {
            ClassOrInterfaceType returnType = new ClassOrInterfaceType(null, "Future")
                    .setTypeArguments(new ClassOrInterfaceType(null, "Void"));
            startAsyncMethod.setType(returnType);
            flowImplClass.addExtendedType(String.format("Flow<%s, %s>", contextSource.getSimpleName(), "Void"));
        }

        startMethod.setThrownExceptions(entryMethod.getThrownExceptions());
        startAsyncMethod.setThrownExceptions(entryMethod.getThrownExceptions());
        reRunMethod.setThrownExceptions(entryMethod.getThrownExceptions());

        BlockStmt methodBody = new BlockStmt();
        startMethod.setBody(methodBody);

        BlockStmt reRunMethodBody = new BlockStmt();
        reRunMethod.setBody(reRunMethodBody);

        BlockStmt startAsyncMethodBody = new BlockStmt();
        startAsyncMethod.setBody(startAsyncMethodBody);
        LambdaExpr lambdaExpr = new LambdaExpr();
        BlockStmt lambdaBody = new BlockStmt();
        lambdaExpr.setBody(lambdaBody);

        methodBody.addStatement(format("%s context = new %s();", contextSource.getSimpleName(), contextSource.getSimpleName()));
        if (MapUtils.isNotEmpty(flow.getCallable().getParams())) {
            methodBody.addStatement(format("%s param = new %s();", flowParamClassName, flowParamClassName));
            for (Param input : flow.getCallable().getParams().values()) {
                methodBody.addStatement(format("param.set%s(%s);", capitalize(input.getName()), input.getName()));
            }
            methodBody.addStatement(format("context.set%s(param);", flowParamClassName));
        }

        //call the start node
        if (null == entryStep) {
            throw new IllegalStateException("entryStep can not be null");
        }
        methodBody.getStatements().forEach(startAsyncMethodBody::addStatement);

        methodBody.addStatement("this.initAndLock(context);");
        reRunMethodBody.addStatement("this.initAndLock(context);");
        lambdaBody.addStatement("this.initAndLock(context);");

        methodBody.addStatement(entryStep.getEntryStmt());
        reRunMethodBody.addStatement(entryStep.getEntryStmt());
        lambdaBody.addStatement(entryStep.getEntryStmt());

        Output flowOutput = flow.getOutput();
        if (flowOutput != null) {
            String flowOutputType = flowOutput.getType().describe();
            if (flowOutputType != null) {
                Imports.of(flowOutput.getType()).install(flowCU);
                FieldBuilder fieldBuilder = FieldBuilder.builder(flow.getName() + "Output", flowOutput.getType())
                        .withSetter()
                        .withGetter();
                if (flowOutput.getType().isReferenceType()) {
                    contextSource.addImport(flowOutputType);
                    fieldBuilder.initializer(new JavaParser().parseExpression(format("new %s()", flowOutputType)).getResult().get());
                }
                contextSource.addField(fieldBuilder);
            }

            if (flowOutput.getType().isVoid()) {
                reRunMethodBody.addStatement("return null;");
                lambdaBody.addStatement("return null;");
            } else {
                reRunMethodBody.addStatement(format("return context.get%sOutput();", flow.getCapitalizeName()));
                methodBody.addStatement(format("return context.get%sOutput();", flow.getCapitalizeName()));
                lambdaBody.addStatement(format("return context.get%sOutput();", flow.getCapitalizeName()));
            }
        }
        ReturnStmt returnStmt = new ReturnStmt(String.format("getExecutor().submit(()%s)", lambdaExpr));
        startAsyncMethodBody.addStatement(returnStmt);
    }

    @Override
    public CompilationUnit getCompilationUnit() {
        return flow.getCompilationUnit();
    }

    @Override
    public String getFileName() {
        return flowImplClass.getNameAsString();
    }

    /**
     * Gets output.
     *
     * @return the output
     */
    public Output getOutput() {
        return flow.getOutput();
    }

    /**
     * Gets returns.
     *
     * @return the returns
     */
    public Iterable<? extends Return> getReturns() {
        return flow.getCallable().getReturns();
    }

    /**
     * Gets simple name.
     *
     * @return the simple name
     */
    public String getSimpleName() {
        return flow.getCapitalizeName();
    }

    /**
     * Gets flow name.
     *
     * @return the flow name
     */
    public String getFlowName() {
        return flow.getName();
    }

    /**
     * Gets input class name.
     *
     * @return the input class name
     */
    public String getInputClassName() {
        return flow.getInputClassName();
    }

    /**
     * Gets exception handler.
     *
     * @return the exception handler
     */
    public ExceptionHandler getExceptionHandler() {
        return flow.getExceptionHandler();
    }
}
